Skip to content

feature: multipart uploads and retry - #246

Merged
madebydavid merged 8 commits into
mainfrom
244-feature-upload-improvements
Aug 26, 2026
Merged

feature: multipart uploads and retry#246
madebydavid merged 8 commits into
mainfrom
244-feature-upload-improvements

Conversation

@madebydavid

@madebydavid madebydavid commented Aug 24, 2026

Copy link
Copy Markdown
Member

This is related to #244

Goals

  • Make use of the new backend multipart upload endpoints
    • Allows for larger uploads, more stable speeds
  • Add a retry mechanism to make more resilient

What's changed

  • Adding multipart upload for ZIP files > 16Mb
  • Multipart upload used by default but be disabled with --skipMultipart
  • Uses the new backend multipart endpoints
  • Retry method in multipart

@madebydavid madebydavid linked an issue Aug 24, 2026 that may be closed by this pull request
@madebydavid
madebydavid marked this pull request as ready for review August 25, 2026 10:22
@madebydavid madebydavid changed the title Draft: feature: upload improvements feature: upload improvements Aug 25, 2026
@madebydavid
madebydavid requested a balanced review from Copilot August 25, 2026 10:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds multipart ZIP uploads and retry handling to improve large-upload resilience.

Changes:

  • Uses multipart uploads for ZIPs ≥16 MiB, with --skipMultipart fallback.
  • Adds signed-part APIs, concurrency limiting, retries, progress, completion, and cleanup.
  • Adds supporting types, dependency, and unit tests.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/api/index.ts Adds multipart API operations.
src/commands/game/ship.tsx Adds the multipart opt-out flag.
src/types/api.ts Defines multipart API types.
src/types/index.ts Extends shipping flags.
src/utils/errors.ts Adds response and retry helpers.
src/utils/ship/index.ts Selects single or multipart upload.
src/utils/ship/multipartUpload.ts Implements multipart upload orchestration.
src/utils/ship/upload.ts Extracts single-upload handling and limits.
test/utils/errors.test.ts Tests retry classification.
test/utils/ship/multipartUpload.test.ts Tests part calculation and retry basics.
package.json Adds p-limit.
package-lock.json Locks the new dependency.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/utils/ship/multipartUpload.ts
Comment thread src/utils/ship/multipartUpload.ts
Comment thread src/utils/ship/multipartUpload.ts
Comment thread src/utils/ship/multipartUpload.ts Outdated
Comment thread src/utils/errors.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

src/utils/ship/multipartUpload.ts:111

  • A short FileHandle.read() does not necessarily mean EOF, so the network-mount case mentioned here can abort an otherwise valid upload. Continue reading into the remaining portion of the buffer and fail only if a zero-byte read occurs before the expected part size is reached.
    const {bytesRead} = await handle.read(body, 0, part.size, part.start)
    // A network mount can return less than it was asked for. Uploading the
    // short buffer would pad the part with zeros and corrupt the zip.
    if (bytesRead !== part.size) {
      throw new Error(`Part ${part.partNumber} read ${bytesRead} bytes, expected ${part.size}`)

src/utils/ship/multipartUpload.ts:233

  • clearQueue() only removes callbacks that have not started; up to eight active callbacks continue reading, PUTting, and reporting progress. This function then rejects immediately, so multipartUpload aborts the ticket and ship deletes the ZIP while those PUTs are still running, potentially uploading parts after cleanup. Signal cancellation and await every in-flight task before rethrowing so abort happens only after uploads have stopped.
    limit.clearQueue()

@madebydavid

Copy link
Copy Markdown
Member Author

src/utils/ship/multipartUpload.ts:111

  • A short FileHandle.read() does not necessarily mean EOF, so the network-mount case mentioned here can abort an otherwise valid upload. Continue reading into the remaining portion of the buffer and fail only if a zero-byte read

@copilot - took what you said onboard - could you review the changes to that part?

Copilot AI commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

src/utils/ship/multipartUpload.ts:111

...

The implementation looks correct. The readPart function now loops until filled === part.size, continuing to read into the remaining portion of the buffer on each iteration. It only breaks out of the loop on a zero-byte read (true EOF), and then a final guard checks that all expected bytes were read before returning — throwing if not. This correctly handles the short-read case from network mounts without prematurely aborting a valid upload.

@madebydavid

Copy link
Copy Markdown
Member Author

I have tested this by uploading a 700MB game zip and disabling my wifi and enabling it again during the upload - I also noted that this works with the old non-multi upload (still usable via --skipMultipart)

@madebydavid madebydavid changed the title feature: upload improvements feature: multipart uploads and retry Aug 25, 2026
@madebydavid

Copy link
Copy Markdown
Member Author

Tried a better test - switching to a mobile hotspot and back during the upload:

david@sal9000:~/work/shipthis.cc/testing/large-game$ shipthis game ship --platform android --useDemoCredentials --follow --verbose 
Fetching game config...
Determining platforms to ship...
Platform specified: ANDROID
Retrieving file globs...
Finding files to include in zip...
Found 736 files, adding to zip...
Creating zip file: shipthis-7689c2c1-8f61-4f39-bc1f-ab5e82f251fd.zip
Zipping: 0% (0.00MB / ~733.06MB) - 0.0s - 0.00MB/s
Zipping: 10% (70.92MB / ~733.06MB) - 2.0s - 35.28MB/s
Zipping: 20% (143.22MB / ~733.06MB) - 4.0s - 35.72MB/s
Zipping: 30% (217.15MB / ~733.06MB) - 6.0s - 36.13MB/s
Zipping: 36% (266.49MB / ~733.06MB) - 8.0s - 33.27MB/s
Zipping: 42% (306.37MB / ~733.06MB) - 10.0s - 30.61MB/s
Zipping: 49% (361.33MB / ~733.06MB) - 12.0s - 30.09MB/s
Zipping: 59% (435.65MB / ~733.06MB) - 14.0s - 31.10MB/s
Zipping: 64% (465.80MB / ~733.06MB) - 16.0s - 29.09MB/s
Zipping: 68% (495.92MB / ~733.06MB) - 18.0s - 27.54MB/s
Zipping: 71% (521.81MB / ~733.06MB) - 20.0s - 26.08MB/s
Zipping: 75% (551.36MB / ~733.06MB) - 22.0s - 25.05MB/s
Zipping: 79% (581.62MB / ~733.06MB) - 24.0s - 24.22MB/s
Zipping: 83% (608.58MB / ~733.06MB) - 26.0s - 23.40MB/s
Uploading zip file...
Requesting multipart upload ticket...
Uploading in parts of 8MB...
Uploading: 1% (8.00MB / 633.16MB) - 1.6s - 5.09MB/s
Uploading: 3% (16.00MB / 633.16MB) - 1.7s - 9.47MB/s
Uploading: 4% (24.00MB / 633.16MB) - 1.8s - 13.25MB/s
Uploading: 5% (32.00MB / 633.16MB) - 2.0s - 15.83MB/s
Uploading: 6% (40.00MB / 633.16MB) - 2.1s - 19.06MB/s
Uploading: 8% (48.00MB / 633.16MB) - 2.2s - 22.10MB/s
Part 5 attempt 1 failed (fetch failed). Retrying in 0.4s...
Uploading: 9% (56.00MB / 633.16MB) - 75.8s - 0.74MB/s
Uploading: 10% (64.00MB / 633.16MB) - 76.9s - 0.83MB/s
Uploading: 11% (72.00MB / 633.16MB) - 78.0s - 0.92MB/s
Uploading: 13% (80.00MB / 633.16MB) - 79.4s - 1.01MB/s
Uploading: 14% (88.00MB / 633.16MB) - 80.5s - 1.09MB/s
Uploading: 15% (96.00MB / 633.16MB) - 81.8s - 1.17MB/s
Uploading: 16% (104.00MB / 633.16MB) - 83.1s - 1.25MB/s
Uploading: 18% (112.00MB / 633.16MB) - 84.4s - 1.33MB/s
Uploading: 19% (120.00MB / 633.16MB) - 86.3s - 1.39MB/s
Cleaning up temporary zip file...

/home/david/work/shipthis.cc/oclif-cli/shipthis/node_modules/@oclif/core/lib/errors/error.js:44
        err = new cli_1.CLIError(input, options);
              ^

CLIError: Part 8 failed: 400 Bad Request
    at Object.error (/home/david/work/shipthis.cc/oclif-cli/shipthis/node_modules/@oclif/core/lib/errors/error.js:44:15)
    at GameShip.error (/home/david/work/shipthis.cc/oclif-cli/shipthis/node_modules/@oclif/core/lib/command.js:216:23)
    at handleError (/home/david/work/shipthis.cc/oclif-cli/shipthis/src/commands/game/ship.tsx:117:12)
    at process.processTicksAndRejections (node:internal/process/task_queues:104:5) {
  code: undefined,
  oclif: { exit: 2 },
  skipOclifErrorHandling: undefined,
  suggestions: undefined
}

@madebydavid

Copy link
Copy Markdown
Member Author

Looking into parsing and retrying that 400

@madebydavid

Copy link
Copy Markdown
Member Author

Updated - the multipart upload survives a network switch mid-upload. It takes a while to resume but seems reliable.

@madebydavid

Copy link
Copy Markdown
Member Author

i will merge this once backend and infra (updated lifecycle rule) changes have been deployed to production.

@madebydavid

Copy link
Copy Markdown
Member Author

@copilot review the latest changes - docs and unhiding the flag

Copilot AI commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

@copilot review the latest changes - docs and unhiding the flag

Changes look good. The --skipMultipart flag is unhidden with the updated description matching the docs. The new docs section clearly explains the multipart threshold (16MB), the retry behaviour, and when to use --skipMultipart including its 5GB single-request limit. No issues found.

@madebydavid
madebydavid merged commit 48015bf into main Aug 26, 2026
1 check passed
@madebydavid
madebydavid deleted the 244-feature-upload-improvements branch August 26, 2026 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feature: upload improvements

3 participants